24张图带你彻底弄懂 Java 线程池 您所在的位置:网站首页 线程池commit和execute 区别 24张图带你彻底弄懂 Java 线程池

24张图带你彻底弄懂 Java 线程池

2024-06-26 19:31| 来源: 网络整理| 查看: 265

# 第二十五节:线程池

好,终于到 Java 的线程池了,这是 Java 并发编程中非常重要的一块内容,今天我们就通过图文的方式来彻底弄懂线程池的工作原理,以及在实际项目中该如何自定义适合业务的线程池。

# 一、什么是线程池

线程池其实是一种池化的技术实现,池化技术的核心思想就是实现资源的复用,避免资源的重复创建和销毁带来的性能开销。线程池可以管理一堆线程,让线程执行完任务之后不进行销毁,而是继续去处理其它线程已经提交的任务。

使用线程池的好处

降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。# 二、线程池的构造

Java 主要是通过构建 ThreadPoolExecutor 来创建线程池的。接下来我们看一下线程池是如何构造出来的

ThreadPoolExecutor 的构造方法:

corePoolSize:线程池中用来工作的核心线程数量。maximumPoolSize:最大线程数,线程池允许创建的最大线程数。keepAliveTime:超出 corePoolSize 后创建的线程存活时间或者是所有线程最大存活时间,取决于配置。unit:keepAliveTime 的时间单位。workQueue:任务队列,是一个阻塞队列,当线程数达到核心线程数后,会将任务存储在阻塞队列中。threadFactory :线程池内部创建线程所用的工厂。handler:拒绝策略;当队列已满并且线程数量达到最大线程数量时,会调用该方法处理任务。

线程池的构造其实很简单,就是传入一堆参数,然后进行简单的赋值操作。

# 三、线程池的运行原理

说完线程池的核心构造参数,接下来来讲解这些参数在线程池中是如何工作的。

线程池刚创建出来是什么样子呢,如下图:

没错,刚创建出来的线程池中只有一个构造时传入的阻塞队列,里面并没有线程,如果想要在执行之前创建好核心线程数,可以调用 prestartAllCoreThreads 方法来实现,默认是没有线程的。

当有线程通过 execute 方法提交了一个任务,会发生什么呢?

首先会去判断当前线程池的线程数是否小于核心线程数,也就是线程池构造时传入的参数 corePoolSize。

如果小于,那么就直接通过 ThreadFactory 创建一个线程来执行这个任务,如图

当任务执行完之后,线程不会退出,而是会去阻塞队列中获取任务,如下图

接下来如果又提交了一个任务,也会按照上述的步骤去判断是否小于核心线程数,如果小于,还是会创建线程来执行任务,执行完之后也会从阻塞队列中获取任务。

这里有个细节,就是提交任务的时候,就算有线程池里的线程从阻塞队列中获取不到任务,如果线程池里的线程数还是小于核心线程数,那么依然会继续创建线程,而不是复用已有的线程。

如果线程池里的线程数不再小于核心线程数呢?那么此时就会尝试将任务放入阻塞队列中,入队成功之后,如图

这样,阻塞的线程就可以获取到任务了。

但是,随着任务越来越多,队列已经满了,任务放入失败,怎么办呢?

此时会判断当前线程池里的线程数是否小于最大线程数,也就是入参时的 maximumPoolSize 参数

如果小于最大线程数,那么也会创建非核心线程来执行提交的任务,如图

所以,就算队列中有任务,新创建的线程还是会优先处理这个提交的任务,而不是从队列中获取已有的任务执行,从这可以看出,先提交的任务不一定先执行。

假如线程数已经达到最大线程数量,怎么办呢?

此时就会执行拒绝策略,也就是构造线程池的时候,传入的 RejectedExecutionHandler 对象,来处理这个任务。

JDK 自带的 RejectedExecutionHandler 实现有 4 种

AbortPolicy:丢弃任务,抛出运行时异常CallerRunsPolicy:由提交任务的线程来执行任务DiscardPolicy:丢弃这个任务,但是不抛异常DiscardOldestPolicy:从队列中剔除最先进入队列的任务,然后再次提交任务

线程池创建的时候,如果不指定拒绝策略就默认是 AbortPolicy 策略。

当然,你也可以自己实现 RejectedExecutionHandler 接口,比如将任务存在数据库或者缓存中,这样就可以从数据库或者缓存中获取被拒绝掉的任务了。

到这里,我们发现,线程池构造的几个参数 corePoolSize、maximumPoolSize、workQueue、threadFactory、handler 我们都在上述的执行过程中讲到了,那么还差两个参数 keepAliveTime 和 unit(unit 是 keepAliveTime 的时间单位)没讲到,所以 keepAliveTime 是如何起作用的呢,这个问题留到后面分析。

说完整个执行的流程,接下来看看 execute 方法的代码是如何实现的。

public void execute(Runnable command) { // 首先检查提交的任务是否为null,是的话则抛出NullPointerException。 if (command == null) throw new NullPointerException(); // 获取线程池的当前状态(ctl是一个AtomicInteger,其中包含了线程池状态和工作线程数) int c = ctl.get(); // 1. 检查当前运行的工作线程数是否少于核心线程数(corePoolSize) if (workerCountOf(c) = STOP || workQueue.isEmpty())) { decrementWorkerCount(); return null; } int wc = workerCountOf(c); // 检查工作线程是否应当在没有任务执行时,经过keepAliveTime之后被终止 boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // 如果工作线程数超出最大线程数或者超出核心线程数且上一次poll()超时,并且队列为空或工作线程数大于1, // 则尝试减少工作线程数 if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { if (compareAndDecrementWorkerCount(c)) return null; continue; } try { // 根据timed标志,决定是无限期等待任务,还是等待keepAliveTime时间 Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : // 指定时间内等待 workQueue.take(); // 无限期等待 if (r != null) // 成功获取到任务 return r; // 如果poll()超时,则设置timedOut标志 timedOut = true; } catch (InterruptedException retry) { // 如果在等待任务时线程被中断,重置timedOut标志并重新尝试获取任务 timedOut = false; } } }

前面就是线程池的一些状态判断,这里有一行代码

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

这行代码是用来判断当前过来获取任务的线程是否可以超时退出。如果 allowCoreThreadTimeOut 设置为 true 或者线程池当前的线程数大于核心线程数,也就是 corePoolSize,那么该获取任务的线程就可以超时退出。

怎么做到超时退出呢,就是这行核心代码

Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();

会根据是否允许超时来选择调用阻塞队列 workQueue 的 poll 方法或者 take 方法。如果允许超时,则调用 poll 方法,传入 keepAliveTime,也就是构造线程池时传入的空闲时间,这个方法的意思就是从队列中阻塞 keepAliveTime 时间来获取任务,获取不到就会返回 null;如果不允许超时,就会调用 take 方法,这个方法会一直阻塞获取任务,直到从队列中获取到任务为止。

从这里就可以看到 keepAliveTime 是如何使用的了。

所以到这里,大家应该知道线程池中的线程为什么可以做到空闲一定时间就退出了吧?

其实最主要就是利用了阻塞队列的 poll 方法,这个方法可以指定超时时间,一旦线程达到了 keepAliveTime 还没有获取到任务,就会返回 null,一旦 getTask 方法返回 null,线程就会退出。

这里也有一个细节,就是判断当前获取任务的线程是否可以超时退出的时候,如果将 allowCoreThreadTimeOut 设置为 true,那么所有线程走到这个 timed 都是 true,所有线程包括核心线程都可以做到超时退出。如果线程池需要将核心线程超时退出,就可以通过 allowCoreThreadTimeOut 方法将 allowCoreThreadTimeOut 变量设置为 true。

整个 getTask 方法以及线程超时退出的机制如图所示

# 六、线程池的 5 种状态

线程池内部有 5 个常量来代表线程池的五种状态

private static final int RUNNING = -1


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有